#include "mpqarchive.h"
namespace Moonlite {
namespace Data {
namespace Mpq
{
	void TMPQArchive::Open(MPQ_ARCHIVE_FORMAT format, unsigned int hashTableSize)
	{
		pin_ptr<HANDLE> handlePtr = &_handle;
		TStringHandle filenameHandle(_filename);

		if (System::IO::File::Exists(_filename))
		{
			if (!SFileCreateArchiveEx(filenameHandle.Value, OPEN_EXISTING, 0, handlePtr))
			{
				throw gcnew System::IO::FileNotFoundException();
			}
		}
		else
		{
			if (hashTableSize < HASH_TABLE_SIZE_MIN)
			{
				throw gcnew System::ArgumentException("Hash table size must be at least " + HASH_TABLE_SIZE_MIN + ".");
			}

			if (hashTableSize > HASH_TABLE_SIZE_MAX)
			{
				throw gcnew System::ArgumentException("Hash table size can be at most " + HASH_TABLE_SIZE_MAX + ".");
			}

			if (!SFileCreateArchiveEx(filenameHandle.Value, BuildArchiveFlags(format), hashTableSize, handlePtr))
			{
				throw gcnew System::IO::IOException("Unable to open or create TMPQArchive at path \"" + _filename + "\".");
			}
		}
	}

	void TMPQArchive::Cleanup(bool managed)
	{
		if (!_disposed)
		{
			UNREFERENCED_PARAMETER(managed);
			if(_handle != NULL)
			{
				SFileCloseArchive(_handle);
				_handle = NULL;
			}

			_disposed = true;
		}
		else
		{
#ifdef SHOW_OBJECTDISPOSED_EXCEPTIONS
			throw gcnew System::ObjectDisposedException(nullptr);
#endif
		}
	}

	void TMPQArchive::CheckBadState()
	{
		if (_disposed)
		{
#ifdef SHOW_OBJECTDISPOSED_EXCEPTIONS
			throw gcnew System::ObjectDisposedException(nullptr);
#endif
		}

		if ((_handle == NULL) || (_handle == INVALID_HANDLE_VALUE))
		{
			throw gcnew System::InvalidOperationException();
		}
	}

	unsigned int TMPQArchive::BuildArchiveFlags(MPQ_ARCHIVE_FORMAT format)
	{
		unsigned int flags = CREATE_ALWAYS;
		switch (format)
		{
			case MPQ_ARCHIVE_FORMAT::VERSION1:
				flags |= MPQ_CREATE_ARCHIVE_V1;
				break;
			case MPQ_ARCHIVE_FORMAT::VERSION2:
				flags |= MPQ_CREATE_ARCHIVE_V2;
				break;
		}

		return flags;
	}

	unsigned int TMPQArchive::BuildFileFlags(MPQ_COMPRESSION compression, MPQ_ENCRYPTION encryption)
	{
		unsigned int flags = MPQ_FILE_REPLACEEXISTING;
		switch (compression)
		{
			case MPQ_COMPRESSION::IMPLODE:
				flags |= MPQ_FILE_IMPLODE;
				break;
			case MPQ_COMPRESSION::HUFFMANN:
				flags |= MPQ_FILE_COMPRESS | MPQ_COMPRESSION_HUFFMANN;
				break;
			case MPQ_COMPRESSION::ZLIB:
				flags |= MPQ_FILE_COMPRESS | MPQ_COMPRESSION_ZLIB;
				break;
			case MPQ_COMPRESSION::PKWAREDCL:
				flags |= MPQ_FILE_COMPRESS | MPQ_COMPRESSION_PKWARE;
				break;
			case MPQ_COMPRESSION::BZIP2:
				flags |= MPQ_FILE_COMPRESS | MPQ_COMPRESSION_BZIP2;
				break;
			case MPQ_COMPRESSION::WAVEMONO:
				flags |= MPQ_FILE_COMPRESS | MPQ_COMPRESSION_WAVE_MONO;
				break;
			case MPQ_COMPRESSION::WAVESTEREO:
				flags |= MPQ_FILE_COMPRESS | MPQ_COMPRESSION_WAVE_STEREO;
				break;
		}

		switch (encryption)
		{
			case MPQ_ENCRYPTION::ENCRYPTED:
				flags |= MPQ_FILE_ENCRYPTED;
				break;
			case MPQ_ENCRYPTION::FIXEDSEED:
				flags |= MPQ_FILE_ENCRYPTED | MPQ_FILE_FIXSEED;
				break;
		}

		return flags;
	}

	unsigned int TMPQArchive::BuildWaveFlags(MPQ_QUALITY quality)
	{
		switch (quality)
		{
			case MPQ_QUALITY::LOW:
				return MPQ_WAVE_QUALITY_LOW;
			case MPQ_QUALITY::MEDIUM:
				return MPQ_WAVE_QUALITY_MEDIUM;
			case MPQ_QUALITY::HIGH:
				return MPQ_WAVE_QUALITY_HIGH;
		}

		return 0;
	}

	void TMPQArchive::Close()
	{
		if (_disposed)
		{
#ifdef SHOW_OBJECTDISPOSED_EXCEPTIONS
			throw gcnew System::ObjectDisposedException(nullptr);
#endif
		}

		Cleanup(true);
	}

	void TMPQArchive::Compact()
	{
		CheckBadState();
		if (!SFileCompactArchive(_handle, NULL, FALSE))
		{
			throw gcnew System::IO::IOException("SFileCompactArchive: compact operation failed.");
		}
	}

	bool TMPQArchive::Exists(System::String^ filename)
	{
		CheckBadState();
		TStringHandle filenameHandle(filename);
		return SFileHasFile(_handle, const_cast<LPSTR>(filenameHandle.Value)) != 0;
	}

	void TMPQArchive::ExportFile(System::String^ archivedName, System::String^ localFileName)
	{
		CheckBadState();
		TStringHandle archivedNameHandle(archivedName);
		TStringHandle localFileNameHandle(localFileName);
		if (!SFileExtractFile(_handle, archivedNameHandle.Value, localFileNameHandle.Value))
		{
			throw gcnew System::IO::IOException("SFileExtractFile: extract file operation failed.");
		}
	}

	void TMPQArchive::ExportFile(System::String^ archivedName, array<System::Byte>^ data)
	{
		CheckBadState();
		ExportFile(archivedName, data, 0);
	}

	void TMPQArchive::ExportFile(System::String^ archivedName, array<System::Byte>^ data, int index)
	{
		CheckBadState();
		TTemporaryFile tempFile;
		ExportFile(archivedName, tempFile.FileName);
		tempFile.CopyTo(data, index);
	}

	void TMPQArchive::Flush()
	{
		CheckBadState();
		if (!SFileFlushArchive(_handle))
		{
			throw gcnew System::IO::IOException("SFileFlushArchive: flush operation failed.");
		}
	}

	System::Collections::Generic::IEnumerable<TMPQFileInfo^>^ TMPQArchive::GetFiles()
	{
		CheckBadState();
		return GetFiles("*", nullptr, false);
	}

	System::Collections::Generic::IEnumerable<TMPQFileInfo^>^ TMPQArchive::GetFiles(System::String^ mask)
	{
		CheckBadState();
		return GetFiles(mask, nullptr, false);
	}

	System::Collections::Generic::IEnumerable<TMPQFileInfo^>^ TMPQArchive::GetFiles(System::String^ mask, bool traverseListFileOnly)
	{
		CheckBadState();
		return GetFiles(mask, nullptr, traverseListFileOnly);
	}

	System::Collections::Generic::IEnumerable<TMPQFileInfo^>^ TMPQArchive::GetFiles(System::String^ mask, System::String^ externalListFile)
	{
		CheckBadState();
		return GetFiles(mask, externalListFile, false);
	}

	System::Collections::Generic::IEnumerable<TMPQFileInfo^>^ TMPQArchive::GetFiles(System::String^ mask, System::String^ externalListFile, bool traverseListFileOnly)
	{
		CheckBadState();
		TStringHandle maskHandle(mask);
		TStringHandle externalListFileHandle(externalListFile != nullptr ? externalListFile : "");
		System::Collections::Generic::List<TMPQFileInfo^>^ files = gcnew System::Collections::Generic::List<TMPQFileInfo^>();
		if (traverseListFileOnly)
		{
			SFILE_FIND_DATA searchData;
			HANDLE searchHandle = SListFileFindFirstFile(_handle, externalListFile != nullptr ? externalListFileHandle.Value : NULL, maskHandle.Value, &searchData);
			if (searchHandle == NULL)
			{
				return files;
			}

			while (searchHandle != NULL)
			{
				files->Add(gcnew TMPQFileInfo(gcnew System::String(searchData.cFileName), searchData.dwFileSize, searchData.dwCompSize));
				if (!SListFileFindNextFile(searchHandle, &searchData))
				{
					break;
				}
			}

			SListFileFindClose(searchHandle);
		}
		else
		{
			SFILE_FIND_DATA searchData;
			HANDLE searchHandle = SFileFindFirstFile(_handle, maskHandle.Value, &searchData, externalListFile != nullptr ? externalListFileHandle.Value : NULL);
			
			if (searchHandle == NULL)
			{
				return files;
			}

			while (searchHandle != NULL)
			{
				files->Add(gcnew TMPQFileInfo(gcnew System::String(searchData.cFileName), searchData.dwFileSize, searchData.dwCompSize));
				if (!SFileFindNextFile(searchHandle, &searchData))
				{
					break;
				}
			}

			SFileFindClose(searchHandle);
		}

		return files;
	}

	void TMPQArchive::ImportFile(System::String^ archivedName, System::String^ localFileName)
	{
		CheckBadState();
		ImportFile(archivedName, localFileName, MPQ_COMPRESSION::NONE, MPQ_ENCRYPTION::NONE);
	}

	void TMPQArchive::ImportFile(System::String^ archivedName, System::String^ localFileName, MPQ_COMPRESSION compression)
	{
		CheckBadState();
		ImportFile(archivedName, localFileName, compression, MPQ_ENCRYPTION::NONE);
	}

	void TMPQArchive::ImportFile(System::String^ archivedName, System::String^ localFileName, MPQ_COMPRESSION compression, MPQ_ENCRYPTION encryption)
	{
		CheckBadState();
		TStringHandle archivedNameHandle(archivedName);
		TStringHandle localFileNameHandle(localFileName);
		if (!SFileAddFile(_handle, localFileNameHandle.Value, archivedNameHandle.Value, BuildFileFlags(compression, encryption)))
		{
			throw gcnew System::IO::IOException("SFileAddFile: add file operation failed.");
		}
	}

	void TMPQArchive::ImportFile(System::String^ archivedName, array<System::Byte>^ data)
	{
		CheckBadState();
		ImportFile(archivedName, data, MPQ_COMPRESSION::NONE, MPQ_ENCRYPTION::NONE);
	}

	void TMPQArchive::ImportFile(System::String^ archivedName, array<System::Byte>^ data, MPQ_COMPRESSION compression)
	{
		CheckBadState();
		ImportFile(archivedName, data, compression, MPQ_ENCRYPTION::NONE);
	}

	void TMPQArchive::ImportFile(System::String^ archivedName, array<System::Byte>^ data, MPQ_COMPRESSION compression, MPQ_ENCRYPTION encryption)
	{
		CheckBadState();
		TTemporaryFile tempFile(data);
		ImportFile(archivedName, tempFile.FileName, compression, encryption);
	}

	void TMPQArchive::ImportListFile(System::String^ filename)
	{
		CheckBadState();
		TStringHandle filenameHandle(filename);
		if (SFileAddListFile(_handle, filenameHandle.Value) != ERROR_SUCCESS)
		{
			throw gcnew System::IO::IOException("SFileAddListFile: add list file operation failed.");
		}
	}

	void TMPQArchive::ImportListFile(array<System::Byte>^ data)
	{
		CheckBadState();
		TTemporaryFile tempFile(data);
		ImportListFile(tempFile.FileName);
	}

	void TMPQArchive::ImportWaveFile(System::String^ archivedName, System::String^ localFileName, MPQ_QUALITY quality)
	{
		CheckBadState();
		ImportWaveFile(archivedName, localFileName, quality, MPQ_COMPRESSION::NONE, MPQ_ENCRYPTION::NONE);
	}

	void TMPQArchive::ImportWaveFile(System::String^ archivedName, System::String^ localFileName, MPQ_QUALITY quality, MPQ_COMPRESSION compression)
	{
		CheckBadState();
		ImportWaveFile(archivedName, localFileName, quality, compression, MPQ_ENCRYPTION::NONE);
	}

	void TMPQArchive::ImportWaveFile(System::String^ archivedName, System::String^ localFileName, MPQ_QUALITY quality, MPQ_COMPRESSION compression, MPQ_ENCRYPTION encryption)
	{
		CheckBadState();
		TStringHandle archivedNameHandle(archivedName);
		TStringHandle localFileNameHandle(localFileName);
		if (!SFileAddWave(_handle, localFileNameHandle.Value, archivedNameHandle.Value, BuildFileFlags(compression, encryption), BuildWaveFlags(quality)))
		{
			throw gcnew System::IO::IOException("SFileAddWave: add wave operation failed.");
		}
	}

	void TMPQArchive::ImportWaveFile(System::String^ archivedName, array<System::Byte>^ data, MPQ_QUALITY quality)
	{
		CheckBadState();
		ImportWaveFile(archivedName, data, quality, MPQ_COMPRESSION::NONE, MPQ_ENCRYPTION::NONE);
	}

	void TMPQArchive::ImportWaveFile(System::String^ archivedName, array<System::Byte>^ data, MPQ_QUALITY quality, MPQ_COMPRESSION compression)
	{
		CheckBadState();
		ImportWaveFile(archivedName, data, quality, compression, MPQ_ENCRYPTION::NONE);
	}

	void TMPQArchive::ImportWaveFile(System::String^ archivedName, array<System::Byte>^ data, MPQ_QUALITY quality, MPQ_COMPRESSION compression, MPQ_ENCRYPTION encryption)
	{
		CheckBadState();
		TTemporaryFile tempFile(data);
		ImportWaveFile(archivedName, tempFile.FileName, quality, compression, encryption);
	}

	void TMPQArchive::RemoveFile(System::String^ archivedName)
	{
		CheckBadState();
		TStringHandle archivedNameHandle(archivedName);
		if (!SFileRemoveFile(_handle, archivedNameHandle.Value, SFILE_OPEN_FROM_MPQ))
		{
			throw gcnew System::IO::IOException("SFileRemoveFile: remove file operation failed.");
		}
	}

	void TMPQArchive::RenameFile(System::String^ archivedName, System::String^ newArchivedName)
	{
		CheckBadState();
		TStringHandle archivedNameHandle(archivedName);
		TStringHandle newArchivedNameHandle(newArchivedName);
		if (!SFileRenameFile(_handle, archivedNameHandle.Value, newArchivedNameHandle.Value))
		{
			throw gcnew System::IO::IOException("SfileRenameFile: rename file operation failed.");
		}
	}

	System::String^ TMPQArchive::ToString()
	{
		CheckBadState();
		return _filename;
	}

	int TMPQArchive::Count::get()
	{
		CheckBadState();
		return SFileGetFileInfo(_handle, SFILE_INFO_NUM_FILES, NULL, 0);
	}

	LCID TMPQArchive::Locale::get()
	{
		CheckBadState();
		return SFileGetLocale();
	}

	void TMPQArchive::Locale::set(LCID locale)
	{
		CheckBadState();
		SFileSetLocale(locale);
	}

	HANDLE TMPQArchive::Handle::get()
	{
		CheckBadState();
		return _handle;
	}

	System::String^ TMPQArchive::FileName::get()
	{
		CheckBadState();
		return _filename;
	}

	System::Object^ TMPQArchive::Tag::get()
	{
		return _tag;
	}

	void TMPQArchive::Tag::set(System::Object^ tag)
	{
		_tag = tag;
	}

	bool TMPQArchive::IsDisposed::get()
	{
		return _disposed;
	}

	TMPQArchive::TMPQArchive(System::String^ filename)
	{
		if (String::IsNullOrEmpty(filename))
		{
			throw gcnew System::ArgumentNullException("filename");
		}

		_filename = filename;
		Open(MPQ_ARCHIVE_FORMAT::VERSION2, DEFAULT_HASH_TABLE_SIZE);
	}

	TMPQArchive::TMPQArchive(System::String^ filename, MPQ_ARCHIVE_FORMAT format)
	{
		if (String::IsNullOrEmpty(filename))
		{
			throw gcnew System::ArgumentNullException("filename");
		}

		_filename = filename;
		Open(format, DEFAULT_HASH_TABLE_SIZE);
	}

	TMPQArchive::TMPQArchive(System::String^ filename, MPQ_ARCHIVE_FORMAT format, unsigned int hashTableSize)
	{
		if (String::IsNullOrEmpty(filename))
		{
			throw gcnew System::ArgumentNullException("filename");
		}

		_filename = filename;
		Open(format, hashTableSize);
	}

	TMPQArchive::~TMPQArchive()
	{
		Cleanup(true);
	}

	TMPQArchive::!TMPQArchive()
	{
		Cleanup(false);
	}
}
}
}